package org.mycollections;

import org.mycollections.myexceptions.MyIndexOutOfBoundsException;

import java.util.Iterator;

public final class MyLinkedList implements MyList, MyStack, MyQueue {

    private static final String INDEX_OUT_OF_BOUNDS_MSG = "Index out of bounds!";

    private static class Node {
        Node prev;
        Node next;
        Object value;
    }

    private final Node head;
    private final Node tail;
    private int size;

    public MyLinkedList() {

        head = new Node();
        tail = new Node();

        head.next = tail;
        tail.prev = head;
    }

    public MyLinkedList(MyList c) {

        this();
        addAll(c.toArray());
        size = c.size();
    }

    void addFirst(Object e) {

        Node newElement = new Node();
        newElement.value = e;
        newElement.prev = head;
        newElement.next = head.next;
        newElement.next.prev = newElement;

        head.next = newElement;

        size++;
    }

    void addLast(Object e) {

        add(e);
    }

    Object getFirst() {

        return head.next.value;
    }

    Object getLast() {

        return tail.prev.value;
    }

    Object removeFirst() {

        Object first = head.next.value;

        head.next.next.prev = head;
        head.next = head.next.next;

        size--;

        return first;
    }

    Object removeLast() {

        Object last = tail.prev.value;

        tail.prev.prev.next = tail;
        tail.prev = tail.prev.prev;

        size--;

        return last;
    }

    @Override
    public void add(Object e) {

        Node newElement = new Node();
        newElement.value = e;
        newElement.next = tail;
        newElement.prev = tail.prev;

        newElement.prev.next = newElement;
        tail.prev = newElement;

        size++;
    }

    @Override
    public void add(int index, Object e) {

        Node n = getNodeByIndex(index);

        Node newElement = new Node();
        newElement.value = e;
        newElement.next = n;
        newElement.prev = n.prev;

        n.prev = newElement;
        newElement.prev.next = newElement;

        size++;
    }

    @Override
    public void addAll(Object[] c) {
        for (Object item : c) {
            add(item);
        }
    }

    @Override
    public void addAll(int index, Object[] c) {

        Node n = getNodeByIndex(index);

        for (Object item : c) {

            Node newElement = new Node();

            newElement.value = item;
            newElement.next = n;
            newElement.prev = n.prev;

            n.prev = newElement;
            newElement.prev.next = newElement;
        }

        size += c.length;
    }

    @Override
    public Object get(int index) {

        Object val = getNodeByIndex(index).value;
        return val;
    }

    @Override
    public Object remove(int index) {

        Node node = getNodeByIndex(index);

        node.prev.next = node.next;
        node.next.prev = node.prev;
        size--;

        return node.value;
    }

    @Override
    public void clear() {

        Node n = head.next;
        while (n != null) {
            Node next = n.next;
            n.value = null;
            n.next = null;
            n.prev = null;
            n = next;
        }

        head.next = tail;
        tail.prev = head;

        size = 0;
    }

    @Override
    public boolean isEmpty() {

        return size == 0;
    }

    @Override
    public void set(int index, Object e) {

        getNodeByIndex(index).value = e;
    }

    @Override
    public int indexOf(Object o) {

        int index = 0;

        Node n = head.next;

        while (n != tail) {
            if (n.value.equals(o)) {
                return index;
            }

            index++;
            n = n.next;
        }

        return -1;
    }

    @Override
    public int size() {

        return size;
    }

    @Override
    public Object[] toArray() {

        Object[] array = new Object[size];
        Node node = head;

        for (int i = 0; i < size; i++) {
            node = node.next;
            array[i] = node.value;
        }

        return array;
    }

    @Override
    public Iterator iterator() {

        final MyList thisObj = this;

        Iterator it = new Iterator() {

            private Node currentNode = head;

            @Override
            public boolean hasNext() {
                return currentNode.next != tail;
            }

            @Override
            public Object next() {
                currentNode = currentNode.next;
                return currentNode.value;
            }

            @Override
            public void remove() {
                Node removingNode = currentNode;
                currentNode = currentNode.prev;
                thisObj.remove(indexOf(removingNode.value));
            }
        };
        return it;
    }

    public Iterator descendingIterator() {

        final MyList thisObj = this;

        Iterator it = new Iterator() {

            private Node currentNode = tail;

            @Override
            public boolean hasNext() {
                return currentNode.prev != head;
            }

            @Override
            public Object next() {
                currentNode = currentNode.prev;
                return currentNode.value;
            }

            @Override
            public void remove() {
                Node removingNode = currentNode;
                currentNode = currentNode.next;
                thisObj.remove(indexOf(removingNode.value));
            }
        };
        return it;
    }

    @Override
    public void offer(Object e) {
        add(e);
    }

    @Override
    public Object peek() {
        Object val = head.next.value;
        return val;
    }

    @Override
    public Object poll() {

        if (head.next == tail) {
            return null;
        }

        Object val = head.next.value;
        removeFirst();
        return val;
    }

    @Override
    public void push(Object e) {
        addFirst(e);
    }

    @Override
    public Object pop() {

        if (head.next == tail) {
            return null;
        }

        Object val = head.next.value;
        removeFirst();
        return val;
    }

    private Node getNodeByIndex(int index) {

        if (index < 0 || index > size - 1) {
            throw new MyIndexOutOfBoundsException(INDEX_OUT_OF_BOUNDS_MSG);
        }

        Node node = head.next;

        int i = 0;
        while (i != index) {
            node = node.next;
            i++;
        }

        return node;
    }

}
